// Copyright 2016 wkh237@github. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.

import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js';
import Log from '../utils/log.js';
import Blob from './Blob.js';
import ProgressEvent from './ProgressEvent.js';
import URIUtil from "../utils/uri";
import {config} from "../fetch";

const log = new Log('XMLHttpRequest');


log.disable();
// log.level(3)

const UNSENT = 0;
const OPENED = 1;
const HEADERS_RECEIVED = 2;
const LOADING = 3;
const DONE = 4;

export default class XMLHttpRequest extends XMLHttpRequestEventTarget {

    _onreadystatechange: () => void;

    upload: XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
    static binaryContentTypes: Array<string> = [
        'image/', 'video/', 'audio/'
    ];

    // readonly
    _readyState: number = UNSENT;
    _uriType: 'net' | 'file' = 'net';
    _response: any = '';
    _responseText: any = '';
    _responseHeaders: any = {};
    _responseType: '' | 'arraybuffer' | 'blob' | 'json' | 'text' = '';
    // TODO : not suppoted ATM
    _responseURL: null = '';
    _responseXML: null = '';
    _status: number = 0;
    _statusText: string = '';
    _timeout: number = 60000;
    _sendFlag: boolean = false;
    _uploadStarted: boolean = false;
    _increment: boolean = false;

    // ReactNativeBlobUtil compatible data structure
    _config: ReactNativeBlobUtilConfig = {};
    _url: any;
    _method: string;
    _headers: any = {
        'Content-Type': 'text/plain'
    };
    _cleanUp: () => void = null;
    _body: any;

    // ReactNativeBlobUtil promise object, which has `progress`, `uploadProgress`, and
    // `cancel` methods.
    _task: any;

    // constants
    get UNSENT() {
        return UNSENT;
    }

    get OPENED() {
        return OPENED;
    }

    get HEADERS_RECEIVED() {
        return HEADERS_RECEIVED;
    }

    get LOADING() {
        return LOADING;
    }

    get DONE() {
        return DONE;
    }

    static get UNSENT() {
        return UNSENT;
    }

    static get OPENED() {
        return OPENED;
    }

    static get HEADERS_RECEIVED() {
        return HEADERS_RECEIVED;
    }

    static get LOADING() {
        return LOADING;
    }

    static get DONE() {
        return DONE;
    }

    static setLog(level: number) {
        if (level === -1)
            log.disable();
        else
            log.level(level);
    }

    static addBinaryContentType(substr: string) {
        for (let i in XMLHttpRequest.binaryContentTypes) {
            if (new RegExp(substr, 'i').test(XMLHttpRequest.binaryContentTypes[i])) {
                return;
            }
        }
        XMLHttpRequest.binaryContentTypes.push(substr);

    }

    static removeBinaryContentType(val) {
        for (let i in XMLHttpRequest.binaryContentTypes) {
            if (new RegExp(substr, 'i').test(XMLHttpRequest.binaryContentTypes[i])) {
                XMLHttpRequest.binaryContentTypes.splice(i, 1);
                return;
            }
        }
    }

    constructor() {
        log.verbose('XMLHttpRequest constructor called');
        super();
    }


    /**
     * XMLHttpRequest.open, always async, user and password not supported. When
     * this method invoked, headers should becomes empty again.
     * @param  {string} method Request method
     * @param  {string} url Request URL
     * @param  {true} async Always async
     * @param  {any} user NOT SUPPORTED
     * @param  {any} password NOT SUPPORTED
     */
    open(method: string, url: string, async: true, user: any, password: any) {
        log.verbose('XMLHttpRequest open ', method, url, async, user, password);
        this._method = method;
        this._url = url;
        this._headers = {};
        this._increment = URIUtil.isJSONStreamURI(this._url);
        this._url = this._url.replace(/^JSONStream\:\/\//, '');
        this._dispatchReadStateChange(XMLHttpRequest.OPENED);
    }

    /**
     * Invoke this function to send HTTP request, and set body.
     * @param  {any} body Body in ReactNativeBlobUtil flavor
     */
    send(body) {

        this._body = body;

        if (this._readyState !== XMLHttpRequest.OPENED)
            throw 'InvalidStateError : XMLHttpRequest is not opened yet.';
        let promise = Promise.resolve();
        this._sendFlag = true;
        log.verbose('XMLHttpRequest send ', body);
        let {_method, _url, _headers} = this;
        log.verbose('sending request with args', _method, _url, _headers, body);
        log.verbose(typeof body, body instanceof FormData);

        if (body instanceof FormData) {
            log.debug('creating blob and setting header from FormData instance');
            body = new Blob(body);
            this._headers['Content-Type'] = `multipart/form-data; boundary=${body.multipartBoundary}`;
        }

        if (body instanceof Blob) {
            log.debug('sending blob body', body._blobCreated);
            promise = new Promise((resolve, reject) => {
                body.onCreated((blob) => {
                    // when the blob is derived (not created by RN developer), the blob
                    // will be released after XMLHttpRequest sent
                    if (blob.isDerived) {
                        this._cleanUp = () => {
                            blob.close();
                        };
                    }
                    log.debug('body created send request');
                    body = URIUtil.wrap(blob.getReactNativeBlobUtilRef());
                    resolve();
                });
            });
        }
        else if (typeof body === 'object') {
            body = JSON.stringify(body);
            promise = Promise.resolve();
        }
        else {
            body = body ? body.toString() : body;
            promise = Promise.resolve();
        }

        promise.then(() => {
            log.debug('send request invoke', body);
            for (let h in _headers) {
                _headers[h] = _headers[h].toString();
            }

            this._task = config({
                auto: true,
                timeout: this._timeout,
                increment: this._increment,
                binaryContentTypes: XMLHttpRequest.binaryContentTypes
            })
                .fetch(_method, _url, _headers, body);
            this._task
                .stateChange(this._headerReceived)
                .uploadProgress(this._uploadProgressEvent)
                .progress(this._progressEvent)
                .catch(this._onError)
                .then(this._onDone);

        });
    }

    overrideMimeType(mime: string) {
        log.verbose('XMLHttpRequest overrideMimeType', mime);
        this._headers['Content-Type'] = mime;
    }

    setRequestHeader(name, value) {
        log.verbose('XMLHttpRequest set header', name, value);
        if (this._readyState !== OPENED || this._sendFlag) {
            throw `InvalidStateError : Calling setRequestHeader in wrong state  ${this._readyState}`;
        }
        // UNICODE SHOULD NOT PASS
        if (typeof name !== 'string' || /[^\u0000-\u00ff]/.test(name)) {
            throw 'TypeError : header field name should be a string';
        }
        //
        let invalidPatterns = [
            /[\(\)\>\<\@\,\:\\\/\[\]\?\=\}\{\s\ \u007f\;\t\0\v\r]/,
            /tt/
        ];
        for (let pattern of invalidPatterns) {
            if (pattern.test(name) || typeof name !== 'string') {
                throw `SyntaxError : Invalid header field name ${name}`;
            }
        }
        this._headers[name] = value;
    }

    abort() {
        log.verbose('XMLHttpRequest abort ');
        if (!this._task)
            return;
        this._task.cancel((err) => {
            let e = {
                timeStamp: Date.now(),
            };
            if (this.onabort)
                this.onabort();
            if (err) {
                e.detail = err;
                e.type = 'error';
                this.dispatchEvent('error', e);
            }
            else {
                e.type = 'abort';
                this.dispatchEvent('abort', e);
            }
        });
    }

    getResponseHeader(field: string): string | null {
        log.verbose('XMLHttpRequest get header', field, this._responseHeaders);
        if (!this._responseHeaders)
            return null;
        return this._responseHeaders[field] || this._responseHeaders[field.toLowerCase()] || null;

    }

    getAllResponseHeaders(): string | null {
        log.verbose('XMLHttpRequest get all headers', this._responseHeaders);
        if (!this._responseHeaders)
            return '';
        let result = '';
        let respHeaders = this.responseHeaders;
        for (let i in respHeaders) {
            result += `${i}: ${respHeaders[i]}${String.fromCharCode(0x0D, 0x0A)}`;
        }
        return result.substr(0, result.length - 2);
    }

    _headerReceived = (e) => {
        log.debug('header received ', this._task.taskId, e);
        this.responseURL = this._url;
        if (e.state === "2" && e.taskId === this._task.taskId) {
            this._responseHeaders = e.headers;
            this._statusText = e.status;
            this._status = Math.floor(e.status);
            this._dispatchReadStateChange(XMLHttpRequest.HEADERS_RECEIVED);
        }
    }

    _uploadProgressEvent = (send: number, total: number) => {
        if (!this._uploadStarted) {
            this.upload.dispatchEvent('loadstart');
            this._uploadStarted = true;
        }
        if (send >= total)
            this.upload.dispatchEvent('load');
        this.upload.dispatchEvent('progress', new ProgressEvent(true, send, total));
    }

    _progressEvent = (send: number, total: number, chunk: string) => {
        log.verbose(this.readyState);
        if (this._readyState === XMLHttpRequest.HEADERS_RECEIVED)
            this._dispatchReadStateChange(XMLHttpRequest.LOADING);
        let lengthComputable = false;
        if (total && total >= 0)
            lengthComputable = true;
        let e = new ProgressEvent(lengthComputable, send, total);

        if (this._increment) {
            this._responseText += chunk;
        }
        this.dispatchEvent('progress', e);
    }

    _onError = (err) => {
        let statusCode = Math.floor(this.status);
        if (statusCode >= 100 && statusCode !== 408) {
            return;
        }
        log.debug('XMLHttpRequest error', err);
        this._statusText = err;
        this._status = String(err).match(/\d+/);
        this._status = this._status ? Math.floor(this.status) : 404;
        this._dispatchReadStateChange(XMLHttpRequest.DONE);
        if (err && String(err.message).match(/(timed\sout|timedout)/) || this._status == 408) {
            this.dispatchEvent('timeout');
        }
        this.dispatchEvent('loadend');
        this.dispatchEvent('error', {
            type: 'error',
            detail: err
        });
        this.clearEventListeners();
    }

    _onDone = (resp) => {
        log.debug('XMLHttpRequest done', this._url, resp, this);
        this._statusText = this._status;
        let responseDataReady = () => {
            log.debug('request done state = 4');
            this.dispatchEvent('load');
            this.dispatchEvent('loadend');
            this._dispatchReadStateChange(XMLHttpRequest.DONE);
            this.clearEventListeners();
        };
        if (resp) {
            let info = resp.respInfo || {};
            log.debug(this._url, info, info.respType);
            switch (this._responseType) {
                case 'blob' :
                    resp.blob().then((b) => {
                        this._responseText = resp.text();
                        this._response = b;
                        responseDataReady();
                    });
                    break;
                case 'arraybuffer':
                    // TODO : to array buffer
                    break;
                case 'json':
                    this._response = resp.json();
                    this._responseText = resp.text();
                    break;
                default :
                    this._responseText = resp.text();
                    this._response = this.responseText;
                    responseDataReady();
                    break;
            }
        }

    }

    _dispatchReadStateChange(state) {
        this._readyState = state;
        if (typeof this._onreadystatechange === 'function')
            this._onreadystatechange();
    }

    set onreadystatechange(fn: () => void) {
        log.verbose('XMLHttpRequest set onreadystatechange', fn);
        this._onreadystatechange = fn;
    }

    get onreadystatechange() {
        return this._onreadystatechange;
    }

    get readyState() {
        log.verbose('get readyState', this._readyState);
        return this._readyState;
    }

    get status() {
        log.verbose('get status', this._status);
        return this._status;
    }

    get statusText() {
        log.verbose('get statusText', this._statusText);
        return this._statusText;
    }

    get response() {
        log.verbose('get response', this._response);
        return this._response;
    }

    get responseText() {
        log.verbose('get responseText', this._responseText);
        return this._responseText;
    }

    get responseURL() {
        log.verbose('get responseURL', this._responseURL);
        return this._responseURL;
    }

    get responseHeaders() {
        log.verbose('get responseHeaders', this._responseHeaders);
        return this._responseHeaders;
    }

    set timeout(val) {
        this._timeout = val * 1000;
        log.verbose('set timeout', this._timeout);
    }

    get timeout() {
        log.verbose('get timeout', this._timeout);
        return this._timeout;
    }

    set responseType(val) {
        log.verbose('set response type', this._responseType);
        this._responseType = val;
    }

    get responseType() {
        log.verbose('get response type', this._responseType);
        return this._responseType;
    }

    static get isRNFBPolyfill() {
        return true;
    }

}
